SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
7.4 KB · 116 lines tsx
Raw Blame History
1import Link from "next/link";2import type { Metadata } from "next";3import { notFound } from "next/navigation";4import { EventRow } from "@/components/event-row";5import { LiveFeed } from "@/components/live-feed";6import { Badge, Bar, Chip, Empty, PageHeader, Panel, Stat, Table, Td, TierBadge } from "@/components/ui";7import { api } from "@/lib/api";8import { feedHref, fmtInt, relTime, typeLabel } from "@/lib/format";910export const dynamic = "force-dynamic";1112/** Section order and labels for the desk (spec §104). Unknown categories are appended in API order. */13const SECTIONS: { key: string; label: string }[] = [14  { key: "government", label: "Government" },15  { key: "finance", label: "Business & markets" },16  { key: "infrastructure", label: "Infrastructure" },17  { key: "health", label: "Health" },18  { key: "news", label: "Media" },19  { key: "cyber", label: "Cyber" },20  { key: "ai", label: "AI" },21  { key: "science", label: "Science" },22];2324export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {25  const { slug } = await params;26  const d = await api.country(slug);27  if (!d) return { title: "Country not found" };28  return { title: `${d.country.name} desk`, description: `Government, business, infrastructure, health, media, cyber, AI and science signals from ${d.country.name}, detected live by WebSensor.`, alternates: { canonical: `/country/${slug}` } };29}3031export default async function CountryPage({ params }: { params: Promise<{ slug: string }> }) {32  const { slug } = await params;33  const d = await api.country(slug);34  if (!d) notFound();35  const code = d.country.code;36  const name = d.country.name;37  const events24 = d.sources.reduce((n, s) => n + (s.events_24h ?? 0), 0);38  const ordered = [...SECTIONS.map((s) => ({ ...s, items: d.by_category.find((c) => c.category === s.key)?.items ?? [] })), ...d.by_category.filter((c) => !SECTIONS.some((s) => s.key === c.category)).map((c) => ({ key: c.category, label: c.category, items: c.items }))];39  const maxType = Math.max(1, ...d.by_type.map((t) => t.n));40  const firstParty = d.sources.filter((s) => s.first_party !== false).length;41  return (42    <>43      <PageHeader44        compact45        kicker={<span className="flex items-center gap-2"><Link href="/country" className="hover:text-fg">Countries</Link> <span className="text-fg-subtle">/</span> <span className="font-mono">{code}</span></span>}46        title={<span className="flex items-center gap-2"><span aria-hidden>{d.country.flag}</span> {name}</span>}47        description={`Official government, business, infrastructure, health and media signals from ${name}, grouped by desk. Sources are the organizations' own channels; media reports are marked EXTERNAL.`}48        actions={<Link href={feedHref({ country: code }, "/live")} className="rounded-md border border-line bg-panel px-2.5 py-1 text-[12px] hover:border-line-strong">Live feed →</Link>}49      />50      <div className="panel mb-4 grid grid-cols-2 divide-x divide-y divide-line sm:grid-cols-4 sm:divide-y-0">51        <Stat label="Sources" value={fmtInt(d.sources.length)} hint={`${fmtInt(firstParty)} first-party · ${fmtInt(d.sources.length - firstParty)} media`} />52        <Stat label="Events · 24 h" value={fmtInt(events24)} tone="signal" hint="meaningful signals" />53        <Stat label="Breaking · 24 h" value={fmtInt(d.breaking.length)} tone={d.breaking.length ? "hot" : undefined} hint="signal ≥ 80" />54        <Stat label="Silent" value={fmtInt(d.silent.length)} tone={d.silent.length ? "silent" : undefined} hint="no matching announcement" />55      </div>5657      <div className="grid gap-4 xl:grid-cols-[1fr_360px]">58        <div className="flex min-w-0 flex-col gap-4">59          <Panel title={<span className="text-hot">Major events · 24 h</span>} dense action={<Link href={feedHref({ country: code, signal_min: 80 }, "/live")} className="text-[11px] text-fg-subtle hover:text-fg">live →</Link>}>60            {d.breaking.length ? d.breaking.map((e) => <EventRow key={e.id} ev={e} />) : <Empty>No breaking signal from {name} in the last 24 h.</Empty>}61          </Panel>62          <div className="grid gap-4 md:grid-cols-2">63            {ordered.map((s) => (64              <Panel key={s.key} title={s.label} dense action={<Link href={feedHref({ country: code, category: s.key }, "/live")} className="text-[11px] text-fg-subtle hover:text-fg">live →</Link>}>65                {s.items.length ? s.items.map((e) => <EventRow key={e.id} ev={e} />) : <Empty>No {s.label.toLowerCase()} signal from {name} recently.</Empty>}66              </Panel>67            ))}68          </div>69          <LiveFeed initial={d.recent} initialCursor={d.nextCursor} extraQuery={{ country: code }} initialFilters={{ country: code }} showTabs={false} title={`LIVE · ${name.toUpperCase()}`} />70        </div>71        <aside className="flex min-w-0 flex-col gap-4">72          <Panel title={<span className="text-silent">Silent changes</span>} dense action={<Link href={feedHref({ country: code, silent_change: true }, "/live")} className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>73            {d.silent.length ? d.silent.map((e) => <EventRow key={e.id} ev={e} />) : <Empty>No silent change from {name}.</Empty>}74          </Panel>75          <Panel title="By type · 7 d" dense>76            {d.by_type.length ? (77              <ul className="divide-y divide-line">78                {d.by_type.map((t) => (79                  <li key={t.event_type} className="grid grid-cols-[9rem_1fr_3.5rem] items-center gap-2 px-3 py-1.5 text-[12.5px]">80                    <Link href={feedHref({ country: code, event_type: t.event_type }, "/live")} className="truncate hover:underline">{typeLabel(t.event_type)}</Link>81                    <Bar value={t.n} max={maxType} tone="info" />82                    <span className="text-right font-mono text-fg-subtle tabular">{fmtInt(t.n)}</span>83                  </li>84                ))}85              </ul>86            ) : (87              <Empty />88            )}89          </Panel>90        </aside>91      </div>9293      <Panel title={`Sources · ${d.sources.length}`} dense className="mt-4" action={<Link href={`/sources?country=${code}`} className="text-[11px] text-fg-subtle hover:text-fg">filter sources →</Link>}>94        {d.sources.length === 0 ? (95          <Empty>No source is attributed to {name} yet.</Empty>96        ) : (97          <Table head={["Tier", "Source", "Domain", "Kind", "Categories", "Sensors", "Events 24 h", "Last event"]}>98            {d.sources.map((s) => (99              <tr key={s.id} className="hover:bg-panel-2/60">100                <Td><TierBadge tier={s.tier} /></Td>101                <Td><Link href={`/source/${s.id}`} className="font-medium hover:underline">{s.name}</Link></Td>102                <Td mono><Link href={`/domain/${s.domain}`} className="text-fg-muted hover:underline">{s.domain}</Link></Td>103                <Td>{s.first_party === false ? <Badge kind="external" compact /> : <Badge kind="first-party" compact />}</Td>104                <Td><div className="flex flex-wrap gap-1">{s.categories.slice(0, 3).map((c) => <Chip key={c} href={`/category/${c}`}>{c}</Chip>)}</div></Td>105                <Td mono>{s.sensor_count ?? 0}</Td>106                <Td mono>{fmtInt(s.events_24h ?? 0)}</Td>107                <Td mono className="text-fg-subtle">{s.last_event_at ? relTime(s.last_event_at) : "—"}</Td>108              </tr>109            ))}110          </Table>111        )}112      </Panel>113    </>114  );115}116